Using APIs from the Frontend

You will often follow an SPA pattern when building widgets or pages. In this pattern, you will provide a frontend resource to be rendered in the browser, the endpoint back to which it calls back for data and the flow which provides the data.

Many get some data from a third party system for the logged in user use cases can be delivered within this pattern.

The UI of the system is served from Flow. This UI markup could be constructed in any framework. In this instance, JQuery has been used to perform minimal data binding to the page.
<html> <head> <!-- We're going to use this for lightweight data binding --> <script type="text/javascript" src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <link rel="stylesheet" href="https://tutorials.pathify.com/styles/main.css"/> <link rel="stylesheet" href="https://tutorials.pathify.com/api/v2/branding/config/active/stylesheet/"/> <!-- Relying on these styles (the ones which ship in your Flow server) will make us visually consistent --> <script type="text/javascript" src="/static/js/helpers/campusWidget.js"></script> <!-- This provides resizing behavior by collaborating with the host portal page --> <script type="text/javascript"> $(function(){ CampusUI.updateHeight(Math.max($("#mainContent").height(),150)); /*Size the container on load*/ $.ajax({ url:'/upstream/googleYourself', method:'GET', success:function(d){ $(".message").text(d.message); $(".when").text(d.when); $("#loading").addClass("hidden"); $(".userEmail").text(d.user.email); $(".googleResponse").attr("srcdoc",d.googleResponse); CampusUI.updateHeight(Math.max($("#mainContent").height(),150)); /*Size the container again after content is repopulated*/ }, error:function(err){ console.log('htmlErr',err); /*Pick your own error handling strategy*/ CampusUI.updateHeight(Math.max($("#mainContent").height(),150)); } }); }); </script> <style> #loading { height:100vh; width:100vw; position:absolute; top:0px; left:0px; right:0px; bottom:0px; z-order:-1; } </style> </head> <body> <div id="mainContent" class="bg-white pa-12"> <div class="h2">This is custom content</div> <div>What: <span class="message"></span></div> <div>When: <span class="when"></span></div> <div>Email: <span class="userEmail"></span></div> <iframe class="googleResponse"></iframe> </div> <div id="loading" class="bg-white"> <div class="h2 ma-12"> Loading... </div> </div> </body> </html>

The serverside behavior to which this UI makes AJAX calls, /upstream/googleYourself, is provided by the googleYourself@tutorials flow.
Two capabilities are uniquely delivered because of the Pathify portal:
  1. Hosted on a subdomain of the portal's location, it can share cookies and shortcut authentication to the same session. This is accomplished with a campusCookieValidator, configured with a configuration reference (found in Shared Config) named tutorials_pathify_cookie_target. This configuration provides a host, path, port and scheme.
  2. Iframed into the portal dashboard, it can communicate its size to have its container resized, removing ugly scrolling. This is accomplished by inter-window messaging, imported from /static/js/campusWidget.js as CampusUI in the global namespace: CampusUI.updateHeight(Math.max($("#mainContent").height(),150));
[ { "name":"auth@tutorials", "steps":[ "checkUser" ], "processors":{ "checkUser":{ "className":"campusCookieValidator", "config":{ "target":{ "referenceObject":"tutorials_pathify_cookie_target" } } } } }, { "name":"googleYourself@tutorials", "steps":[ "detectUser", "fetchFromGoogle", "http", "cacheGoogleResponse", "createResponse" ], "processors":{ "cacheGoogleResponse":{ "className":"dataMapper", "config":{ "jsFunc":"payload.put('googleResponse',new JavaString(item.body().get(),'UTF-8')); return item;" } }, "fetchFromGoogle":{ "className":"jsDataStreamCreator", "config":{ "jsFunc":"var user = fromJValue(payload.get('campusCookieValidatorUserJValue').get()); var req = new code_model_flows_processors_http_HttpRequest( 'https', //scheme // java.lang.String:String 'www.google.com', //host, // java.lang.String:String 443, //port, // java.lang.Int:Int newList(['search']), //path, // scala.collection.immutable.List[java.lang.String:String] 'GET', // verb, // java.lang.String:String toKVList([ ['q',user.name] ]), ////params, // scala.collection.immutable.List[scala.Tuple2[java.lang.String:Stringjava.lang.String:String]] toKVList([ ]) //headers, // scala.collection.immutable.List[scala.Tuple2[java.lang.String:Stringjava.lang.String:String]] ) return [req];" } }, "detectUser":{ "className":"campusCookieValidator", "config":{ "permitUnauthenticated":true, "target":{ "referenceObject":"tutorials_pathify_cookie_target" } } }, "http":{ "className":"httpStreamProviderConfiguration", "config":{ } }, "createResponse":{ "className":"jsDataStreamCreator", "config":{ "jsFunc":"var user = fromJValue(payload.get('campusCookieValidatorUserJValue').get()); var body = { when:new Date().getTime(), message:'clock message', user:user }; payload.get('googleResponse').foreach(function(gr){ body.googleResponse = gr; }); var bodyString = JSON.stringify(body); var resp = new code_model_flows_processors_http_HttpResponse( 207, // java.lang.Int:Int toKVList([ ['Content-Type','application/json'], ['customHeader1','customHeaderValue1'], ['customHeader1','customHeaderValue1'] ]), // scala.collection.immutable.List[scala.Tuple2[java.lang.String:Stringjava.lang.String:String]] Some(bodyString.getBytes('UTF-8')) // scala.Option[scala.Array[scala.Byte]] ) return [resp];" } } } } ]

The actual endpoint, which is a Stateful Behavior of Flow, declared in full as googleYourself. This listens under the Flow server's domain, on its configured path.

[ { "classPath":"http", "config":{ "name":"googleYourself", "path":"/upstream/googleYourself", "orchestratorName":"googleYourself@tutorials", "id":"googleYourself", "isRunning":true, "orchestrationTimeout":180000 } } ]